# This module contains functions required for the native core module
# to be initialized.

from __future__ import print_function
from __future__ import unicode_literals

import sys
import weakref
import logging
import traceback
import stackless

__all__ = [
    # Constants
    'LOG_LEVEL',
    # Exception types
    'KharliaException',
    'ResolutionError',
    # Runctions
    'FindGlobal',
    'FindGlobalF',
    'ClearResolvedNameCache',
    'GetFullClassName',
    'ConstructObject',
    'PrintTaskletExc',
    'IsCurrentTaskletMain',
    'SetTraceFunc',
    'GetTraceFunc',
    'TraceDispatchWrapper',
    # Classes
    'CTaskletExt',
    ]

CACHE_RESOLVED_NAMES = True

GResolvedNames = weakref.WeakValueDictionary()
GResolverExcOutputFunc = print
GTraceFunc = None

if sys.executable.endswith('_d.exe'):
    LOG_LEVEL = logging.DEBUG
else:
    LOG_LEVEL = logging.INFO

class KharliaException(Exception):
    pass

class ResolutionError(KharliaException):
    '''
    Raised when a global object name fails to resolve.
    '''
    pass

def _FindGlobalImpl(name):
    '''_FindGlobalImpl(Name) -> object

    Resolves a dotted Name to a global object. Copied from Python 2.7
    logging module.
    '''
    if not isinstance(name, basestring):
        raise TypeError('Name must be a string')
    try:
        sname = name.split('.')
        used = sname.pop(0)
        found = __import__(used)
        for n in sname:
            used = ''.join((used, '.', n))
            try:
                found = getattr(found, n)
            except AttributeError as e:
                __import__(used)
                found = getattr(found, n)
        return found
    except ImportError as e:
        #if GResolverExcOutputFunc is not None:
        #    GResolverExcOutputFunc(traceback.format_exc())
        raise ResolutionError('failed to resolve \'{0}\': {1}'.format(name, e))

# Global name resolution, needed for configuration files and 
# constructing objects in native code by name

def FindGlobal(name):
    '''FindGlobal(Name) -> object

    Wrapper for _FindGlobalImpl(). Caches results using weak refs.
    '''
    if CACHE_RESOLVED_NAMES:
        target = GResolvedNames.get(name, None)
        if target is None:
            target = _FindGlobalImpl(name)
            try:
                GResolvedNames[name] = target
                #print('FindGlobal: found {0} from resolution, cached'.format(name))
            except TypeError: # If target doesn't support weak references
                #print('FindGlobal: found {0} from resolution, cache fail'.format(name))
                pass
        else:
            #print('FindGlobal: found {0} in cache'.format(name))
            pass
    else:
        target = _FindGlobalImpl(name)
        #print('FindGlobal: found {0} from resolution, no cache'.format(name))
    return target

def FindGlobalF(name):
    try:
        return FindGlobal(name), None
    except ResolutionError as ex:
        assert ex
        return None, ex

def ClearResolvedNameCache():
    global GResolvedNames
    GResolvedNames = weakref.WeakValueDictionary()

def GetFullClassName(obj):
    cls = type(obj)
    return cls.__module__ + '.' + cls.__name__

# This is used by native code to construct instances of CObject
def ConstructObject(name, *args, **kwds):
    '''ConstructObject(name, *args, **kwds) -> object
    
    Used by _core to construct objects from a type name.
    '''
    if isinstance(name, type):
        return name(*args, **kwds)
    else:
        cls = FindGlobal(name)
        if not isinstance(cls, type):
            raise TypeError('Name must resolve to a class')
        return cls(*args, **kwds)

def PrintTaskletExc(out, target=None):
    '''PrintTaskletExc(out, target=None)
    
    This function is for use by exception handlers in tasklets in order to
    print a logger-friendly exception messages. Out is the output callable
    which is called for every line in the message (not including a
    trailing newline). Target is the function or method that the tasklet
    targetted.
    '''
    extype, exvalue, extb = sys.exc_info()
    head = '--- Tasklet Exception: {0} ---'.format(extype.__name__)
    out(head)
    # Traceback formatting
    lf1 = '  File "{0}", line {1}, in {2}'
    lf2 = '    {0}'
               
    out('Traceback:')     
    for line in traceback.extract_tb(extb):
        fs = lf1.format(line[0], line[1], line[2])
        out(fs)
        fs = lf2.format(line[3])
        out(fs)
                
    if target is None:
        target = getattr(stackless.getcurrent(), 'target', None)
    targetstr = str(target) if target is not None else '<unspecified>'
    out('Tasklet target: ' + targetstr)
    out('Message: ' + str(exvalue))
    out('-' * len(head))

def GetTraceFunc():
    return GTraceFunc

def SetTraceFunc(func):
    global GTraceFunc
    GTraceFunc = func

def TraceDispatchWrapper(*args, **kwds):
    '''Wraps the actual trace dispatcher.
    
    This exists to allow tasklets to pickle this function rather than the
    bound instance method of the PyDev dispatcher which is set in
    GTraceFunc.
    
    If GTraceFunc is None when this is called, sys.settrace(None) is
    called.
    '''
    if GTraceFunc:
        return GTraceFunc(*args, **kwds)
    else:
        sys.settrace(None)
        return

class CTaskletExt(stackless.tasklet):
    __slots__ = ['_target', 'critical']
    
    ExcPrintFunc = print
    ExcPrintFuncCritical = print
    UseTrace = False
    SetTraceImpl = staticmethod(sys.settrace)
    
    def __init__(self, target=None):
        stackless.tasklet.__init__(self)
        self._target = None
        self.critical = False
        if target is not None:
            self.bind(target)
    
    @property
    def target(self):
        if self._target:
            return self._target()
        else:
            return None

    @target.setter
    def target(self, value):
        try:
            self._target = weakref.ref(value)
        except TypeError:
            pass

    def __setstate__(self, state):
        #print('UNPICKLING TASKLET NORMAL')
        if 0:
            if self.UseTrace:
                # Set trace functions
                frames = state[0][3]
                for frame in frames:
                    try:
                        frame.f_trace = TraceDispatchWrapper
                    except AttributeError:
                        pass
        stackless.tasklet.__setstate__(self, state[0])
        self.target = state[1]
        self.critical = state[2]
    
    # NOTE: Stackless tasklets don't have getstate functions, instead
    #       their state is returned in __reduce__
    def __reduce_ex__(self, protocol):
        # TODO: Find a good way to exclude trace functions from
        #       pickled frames
        assert protocol == 2
        out = stackless.tasklet.__reduce__(self)
        assert len(out) == 3
        # Check that our trace function is the wrapper, because we
        # don't want to pickle a non-global trace function.
        #
        # The PyDev debugger creates per-frame trace functions which
        # are bound methods, we don't want to pickle these. The global
        # GTraceFunc called by TraceDispatchWrapper should always provide
        # a frame-neutral trace function or create a frame-specific
        # dispatch function.
        #
        # Here, we replace each frame's specific trace with the wrapper,
        # so that when they're unpickled, PyDev will create a new trace
        # for each frame.
        frames = out[2][3]
        cframe = stackless.cframe
        if 0:
            # Check if the last frame is for restoring tracing, and delete it
            if len(frames) >= 2:
                last = frames[-1]
                lastback = last.f_back
                if (isinstance(last, cframe) and 
                        not isinstance(lastback, cframe) and
                        lastback.f_trace):
                    del frames[-1]
            # Clear trace functions from frames
            for frame in frames:
                if not isinstance(frame, cframe):
                    frame.f_trace = None
        if 1:
            for frame in frames:
                if not isinstance(frame, cframe):
                    if frame.f_trace:
                        frame.f_trace = TraceDispatchWrapper
        return out[0], out[1], (out[2], self.target, self.critical)            
    
    def bind(self, target):
        self.target = target

        # Tasklets have their own stack without a trace function
        # by default, so we must set it when the tasklet is created.
        def ext_startup(*args, **kwds):
            try:
                if self.UseTrace:
                    assert GTraceFunc is not None
                    CTaskletExt.SetTraceImpl(TraceDispatchWrapper)
                target(*args, **kwds)
            except Exception:
                if self.critical:
                    PrintTaskletExc(CTaskletExt.ExcPrintFuncCritical, target)
                    raise
                else:
                    PrintTaskletExc(CTaskletExt.ExcPrintFunc, target)
                    
        stackless.tasklet.bind(self, ext_startup)
    
    @staticmethod
    def GetCurrent():
        current = stackless.getcurrent()
        if not isinstance(current, CTaskletExt):
            return None
        return current
    
    @staticmethod
    def SetCurrentCritical(value):
        '''
        Set the critical flag for the current tasklet.
    
        Returns True if the current tasklet is an instance of CTaskletExt and
        the change was successful. Returns False if the current tasklet is not
        an instance of CTaskletExt.
        '''
        current = stackless.getcurrent()
        if not isinstance(current, CTaskletExt):
            return False
        current.critical = value
        return True

    @classmethod
    def Init(cls, errorfunc=print, critfunc=print, tracefunc=None):
        assert callable(errorfunc) and callable(critfunc)
        cls.ExcPrintFunc = errorfunc
        cls.ExcPrintFuncCritical = critfunc
        if tracefunc:
            assert callable(tracefunc)
            cls.UseTrace = True
            cls.SetTraceImpl = tracefunc

    @classmethod
    def Exit(cls):
        cls.ExcPrintFunc = print
        cls.ExcPrintFuncCritical = print
        cls.UseTrace = False
        cls.SetTraceImpl = sys.settrace


def IsCurrentTaskletMain():
    return stackless.getcurrent().is_main

class CEventChannel(object):
    __slots__ = ['Channel']
    
    def __init__(self):
        self.Channel = c = stackless.channel()
        c.preference = 1    # Prefer sender
        
    def Listen(self):
        return self.Channel.receive()
        
    def Dispatch(self, value=None):
        channel = self.Channel
        assert channel.balance < 1 # Should be no senders
        while channel.balance != 0:
            channel.send(value)
            
    __call__ = Dispatch